package com.dbflow5.processor.definition;

import com.dbflow5.StringUtils;
import com.dbflow5.annotation.ConflictAction;
import com.dbflow5.processor.ClassNames;
import com.dbflow5.processor.MethodDefinition;
import com.dbflow5.processor.ProcessorManager;
import com.dbflow5.processor.definition.column.ColumnAccessCombiner;
import com.dbflow5.processor.definition.column.ColumnDefinition;
import com.dbflow5.processor.utils.CodeExtensions;
import com.dbflow5.processor.utils.JavaPoetExtensions;
import com.dbflow5.processor.utils.ModelUtils;
import com.squareup.javapoet.*;

import javax.lang.model.element.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

/**
 * Description:
 *
 * @author Andrew Grosner (fuzz)
 */
public class Methods{

    /**
     * Description: Writes the bind to content values method in the ModelDAO.
     */
    public static class BindToContentValuesMethod implements MethodDefinition {
        public static final String PARAM_CONTENT_VALUES = "values";

        private final EntityDefinition entityDefinition;
        private final boolean isInsert;
        private final boolean implementsContentValuesListener;

        public BindToContentValuesMethod(EntityDefinition entityDefinition, boolean isInsert, boolean implementsContentValuesListener) {
            this.entityDefinition = entityDefinition;
            this.isInsert = isInsert;
            this.implementsContentValuesListener = implementsContentValuesListener;
        }

        public MethodSpec methodSpec() {
            MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(isInsert? "bindToInsertValues" : "bindToContentValues")
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addParameter(ClassNames.CONTENT_VALUES, PARAM_CONTENT_VALUES)
                    .addParameter(entityDefinition.parameterClassName(), ModelUtils.variable)
                    .returns(TypeName.VOID);

            MethodSpec.Builder retMethodBuilder = methodBuilder;

            if (isInsert) {
                entityDefinition.columnDefinitions.forEach(columnDefinition -> {
                    if (!(columnDefinition.type instanceof ColumnDefinition.Type.PrimaryAutoIncrement)
                            && !(columnDefinition.type instanceof ColumnDefinition.Type.RowId)) {
                        methodBuilder.addCode(columnDefinition.contentValuesStatement());
                    }
                });

                if (implementsContentValuesListener) {
                    methodBuilder.addStatement("$L.onBindTo$LValues($L)",
                            ModelUtils.variable, isInsert? "Insert" : "Content", PARAM_CONTENT_VALUES);
                }
            } else {
                if (entityDefinition.primaryKeyColumnBehavior().hasAutoIncrement
                        || entityDefinition.primaryKeyColumnBehavior().hasRowID) {
                    ColumnDefinition autoIncrement = entityDefinition.primaryKeyColumnBehavior().associatedColumn;
                    if(autoIncrement != null){
                        methodBuilder.addCode(autoIncrement.contentValuesStatement());
                    }
                } else if (!implementsContentValuesListener) {
                    retMethodBuilder = null;
                }

                methodBuilder.addStatement("bindToInsertValues($L, $L)", PARAM_CONTENT_VALUES, ModelUtils.variable);
                if (implementsContentValuesListener) {
                    methodBuilder.addStatement("$L.onBindTo$LValues($L)",
                            ModelUtils.variable, isInsert? "Insert" : "Content", PARAM_CONTENT_VALUES);
                }
            }

            return retMethodBuilder != null? retMethodBuilder.build() : null;
        }
    }

    /**
     * Description:
     */
    public static class BindToStatementMethod implements MethodDefinition {
        public static final String PARAM_STATEMENT = "statement";

        private final TableDefinition tableDefinition;
        private final Mode mode;

        public BindToStatementMethod(TableDefinition tableDefinition, Mode mode) {
            this.tableDefinition = tableDefinition;
            this.mode = mode;
        }

        public enum Mode {
            INSERT {
                @Override
                String methodName() {
                    return "bindToInsertStatement";
                }

                @Override
                String sqlListenerName() {
                    return "onBindToInsertStatement";
                }
            },
            UPDATE {
                @Override
                String methodName() {
                    return "bindToUpdateStatement";
                }

                @Override
                String sqlListenerName() {
                    return "onBindToUpdateStatement";
                }
            },
            DELETE {
                @Override
                String methodName() {
                    return "bindToDeleteStatement";
                }

                @Override
                String sqlListenerName() {
                    return "onBindToDeleteStatement";
                }
            };

            abstract String methodName();

            abstract String sqlListenerName();
        }

        @Override
        public MethodSpec methodSpec() {
            MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(mode.methodName())
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addParameter(ClassNames.DATABASE_STATEMENT, PARAM_STATEMENT)
                    .addParameter(tableDefinition.parameterClassName(),
                            ModelUtils.variable).returns(TypeName.VOID);

            // attach non rowid first, then go onto the WHERE clause
            if(mode == Mode.INSERT) {
                AtomicInteger start = new AtomicInteger(1);
                tableDefinition.sqlColumnDefinitions().forEach(columnDefinition -> {
                    methodBuilder.addCode(columnDefinition.getSQLiteStatementMethod(start, true));
                    start.incrementAndGet();
                });
            }else if(mode == Mode.UPDATE) {
                AtomicInteger realCount = new AtomicInteger(1);
                // attach non rowid first, then go onto the WHERE clause
                tableDefinition.sqlColumnDefinitions().forEach(columnDefinition -> {
                    methodBuilder.addCode(columnDefinition.getSQLiteStatementMethod(realCount, true));
                    realCount.incrementAndGet();
                });
                tableDefinition.primaryColumnDefinitions().forEach(columnDefinition -> {
                    methodBuilder.addCode(columnDefinition.getSQLiteStatementMethod(realCount,false));
                    realCount.incrementAndGet();
                });
            }else if(mode == Mode.DELETE) {
                AtomicInteger realCount2 = new AtomicInteger(1);
                tableDefinition.primaryColumnDefinitions().forEach(columnDefinition -> {
                    methodBuilder.addCode(columnDefinition.getSQLiteStatementMethod(realCount2, true));
                    realCount2.incrementAndGet();
                });
            }

            if (tableDefinition.implementsSqlStatementListener) {
                methodBuilder.addStatement(ModelUtils.variable + "."+mode.sqlListenerName()+"("+PARAM_STATEMENT+")");
            }

            return methodBuilder.build();
        }
    }

    /**
     * Description:
     */
    public static class CreationQueryMethod implements MethodDefinition {
        private final TableDefinition tableDefinition;

        public CreationQueryMethod(TableDefinition tableDefinition) {
            this.tableDefinition = tableDefinition;
        }

        @Override
        public MethodSpec methodSpec() {
            return JavaPoetExtensions.overrideFun(String.class, "getCreationQuery", builder -> {
                builder.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
                if (tableDefinition.type.isVirtual()) {
                    CodeBlock codeBlock = CodeExtensions.codeBlock(builder1 -> {
                        builder1.add("CREATE VIRTUAL TABLE IF NOT EXISTS " + StringUtils.quote(tableDefinition.associationalBehavior().name) + " USING ");
                        if (tableDefinition.type == TableDefinition.Type.FTS4) {
                            builder1.add("FTS4");
                        } else if (tableDefinition.type == TableDefinition.Type.FTS3) {
                            builder1.add("FTS3");
                        } else {
                            ProcessorManager.manager.logError("Invalid table type found " + tableDefinition.type);
                        }

                        builder1.add("(");
                        // FTS4 uses column names directly.
                        StringBuilder sb = new StringBuilder();
                        tableDefinition.columnDefinitions.forEach(columnDefinition -> sb.append(StringUtils.quote(columnDefinition.columnName)));
                        builder1.add(sb.toString());
                        tableDefinition.ftsBehavior.addContentTableCode(!tableDefinition.columnDefinitions.isEmpty(), builder1);
                        builder1.add(")");
                        return builder1;
                    });
                    String S = "\"" + codeBlock + "\"";
                    builder.addCode("return " + S + ";\\n");
                } else {
                    int foreignSize = tableDefinition.foreignKeyDefinitions.size();

                    CodeBlock creationBuilder = CodeExtensions.codeBlock(builder1 -> {
                        builder1.add("CREATE " + (tableDefinition.temporary ? "TEMP " : "") + "TABLE IF NOT EXISTS " + StringUtils.quote(tableDefinition.associationalBehavior().name) + "(");

                        StringBuilder sb = new StringBuilder();
                        tableDefinition.columnDefinitions.forEach(columnDefinition -> sb.append(columnDefinition.creationName().toString()));

                        builder1.add(sb.toString());
                        tableDefinition.uniqueGroupsDefinitions.forEach(uniqueGroupsDefinition -> {
                            if (!uniqueGroupsDefinition.columnDefinitionList.isEmpty()) {
                                builder1.add(uniqueGroupsDefinition.creationName());
                            }
                        });

                        if (!tableDefinition.primaryKeyColumnBehavior().hasAutoIncrement) {
                            int primarySize = tableDefinition.primaryColumnDefinitions().size();
                            if (primarySize > 0) {
                                StringBuilder sb2 = new StringBuilder();
                                tableDefinition.primaryColumnDefinitions().forEach(columnDefinition -> {
                                    sb2.append(columnDefinition.primaryKeyName());
                                });
                                builder1.add(", PRIMARY KEY(" + sb2.toString() + ")");
                                if (!tableDefinition.primaryKeyConflictActionName.isEmpty()) {
                                    builder1.add(" ON CONFLICT " + tableDefinition.primaryKeyConflictActionName);
                                }
                            }
                        }
                        if (foreignSize == 0) {
                            builder1.add(")");
                        }
                        return builder1;
                    });

                    CodeBlock.Builder codeBuilder = CodeBlock.builder().add("return \"" + creationBuilder);

                    tableDefinition.foreignKeyDefinitions.forEach(fk -> {
                        EntityDefinition referencedTableDefinition = ProcessorManager.manager.getReferenceDefinition(tableDefinition.associationalBehavior().databaseTypeName, fk.referencedClassName);
                        if (referencedTableDefinition == null) {
                            fk.throwCannotFindReference();
                        } else {
                            StringBuilder buildString = new StringBuilder();
                            buildString.append(", FOREIGN KEY(");
                            buildString.append(StringUtils.joinToString(fk.referenceDefinitionList(), ", ", referenceDefinition -> StringUtils.quote(referenceDefinition.columnName())));
                            buildString.append(") REFERENCES ");
                            buildString.append(referencedTableDefinition.associationalBehavior().name + " ");
                            buildString.append("(");
                            buildString.append(StringUtils.joinToString(fk.referenceDefinitionList(), ", ", referenceDefinition -> StringUtils.quote(referenceDefinition.foreignColumnName)));
                            buildString.append(") ON UPDATE " + fk.foreignKeyColumnBehavior.onUpdate.name().replace("_", " "));
                            buildString.append(" ON DELETE " + fk.foreignKeyColumnBehavior.onDelete.name().replace("_", " "));
                            if (fk.foreignKeyColumnBehavior.deferred) {
                                buildString.append(" DEFERRABLE INITIALLY DEFERRED");
                            }

                            codeBuilder.add(buildString.toString());
                        }
                    });

                    tableDefinition.foreignKeyDefinitions.forEach(fk -> {
                        EntityDefinition referencedTableDefinition = ProcessorManager.manager.getReferenceDefinition(tableDefinition.associationalBehavior().databaseTypeName, fk.referencedClassName);
                        if (referencedTableDefinition == null) {
                            fk.throwCannotFindReference();
                        } else {
                            StringBuilder buildString = new StringBuilder();
                            buildString.append(", FOREIGN KEY(");
                            buildString.append(StringUtils.joinToString(fk.referenceDefinitionList(), ", ", referenceDefinition -> StringUtils.quote(referenceDefinition.columnName())));
                            buildString.append(") REFERENCES ");
                            buildString.append(referencedTableDefinition.associationalBehavior().name + " ");
                            buildString.append("(");
                            buildString.append(StringUtils.joinToString(fk.referenceDefinitionList(), ", ", referenceDefinition -> StringUtils.quote(referenceDefinition.foreignColumnName)));
                            buildString.append(") ON UPDATE " + fk.foreignKeyColumnBehavior.onUpdate.name().replace("_", " "));
                            buildString.append(" ON DELETE $" + fk.foreignKeyColumnBehavior.onDelete.name().replace("_", " "));
                            if (fk.foreignKeyColumnBehavior.deferred) {
                                buildString.append(" DEFERRABLE INITIALLY DEFERRED");
                            }

                            codeBuilder.add(buildString.toString());
                        }
                    });

                    if (foreignSize > 0) {
                        codeBuilder.add(")");
                    }
                    codeBuilder.add("\";\n");

                    builder.addCode(codeBuilder.build());
                }
                return null;
            });
        }
    }

    /**
     * Description: Writes out the custom type converter fields.
     */
    public static class CustomTypeConverterPropertyMethod implements Adders.TypeAdder, Adders.CodeAdder {
        private final EntityDefinition entityDefinition;

        public CustomTypeConverterPropertyMethod(EntityDefinition entityDefinition) {
            this.entityDefinition = entityDefinition;
        }

        @Override
        public void addToType(TypeSpec.Builder typeBuilder) {
            Set<ClassName> customTypeConverters = entityDefinition.associatedTypeConverters.keySet();
            customTypeConverters.forEach(className -> {
                FieldSpec.Builder builder = FieldSpec.builder(className, "typeConverter" + className.simpleName()).addModifiers(Modifier.PRIVATE, Modifier.FINAL);
                builder.initializer(CodeBlock.builder().add("new $T()", className).build());
                typeBuilder.addField(builder.build());
            });

            Set<ClassName> globalTypeConverters = entityDefinition.globalTypeConverters.keySet();
            globalTypeConverters.forEach(className -> {
                FieldSpec.Builder builder = FieldSpec.builder(className, "global_typeConverter" + className.simpleName()).addModifiers(Modifier.PRIVATE, Modifier.FINAL);
                typeBuilder.addField(builder.build());
            });
        }

        @Override
        public CodeBlock.Builder addCode(CodeBlock.Builder code) {
            // Constructor code
            Set<ClassName> globalTypeConverters = entityDefinition.globalTypeConverters.keySet();
            globalTypeConverters.forEach(className -> {
                List<ColumnDefinition> def = entityDefinition.globalTypeConverters.get(className);
                if(def != null) {
                    ColumnDefinition firstDef = def.get(0);
                    if(firstDef != null) {
                        List<TypeName> typeNames = firstDef.typeConverterElementNames();
                        typeNames.forEach(typeName -> code.addStatement("global_typeConverter "+ className.simpleName() +
                                "= ($T) holder.getTypeConverterForClass($T.class)", className, typeName));
                    }
                }
            });
            return code;
        }
    }

    /**
     * Description:
     */
    public static class ExistenceMethod implements MethodDefinition {
        private final EntityDefinition tableDefinition;

        public ExistenceMethod(EntityDefinition tableDefinition) {
            this.tableDefinition = tableDefinition;
        }

        @Override
        public MethodSpec methodSpec() {
            if (!tableDefinition.primaryColumnDefinitions().isEmpty()) {
                ColumnDefinition primaryColumn = tableDefinition.primaryKeyColumnBehavior().associatedColumn;
                if(primaryColumn == null) {
                    primaryColumn =  tableDefinition.primaryColumnDefinitions().get(0);
                }

                if (primaryColumn.shouldWriteExistence()) {
                    CodeBlock.Builder codeBuilder = CodeBlock.builder();
                    primaryColumn.appendExistenceMethod(codeBuilder);

                    return MethodSpec
                            .methodBuilder("exists")
                            .returns(TypeName.BOOLEAN)
                            .addParameter(ParameterSpec.builder(tableDefinition.parameterClassName(), ModelUtils.variable).build())
                            .addParameter(ParameterSpec.builder(ClassNames.DATABASE_WRAPPER, "wrapper").build())
                            .addAnnotation(Override.class)
                            .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                            .addCode(codeBuilder.build())
                            .build();
                }
            }
            return null;
        }
    }

    /**
     * Description:
     */
    public static class InsertStatementQueryMethod implements MethodDefinition {

        private final TableDefinition tableDefinition;
        private final Mode mode;

        public InsertStatementQueryMethod(TableDefinition tableDefinition, Mode mode) {
            this.tableDefinition = tableDefinition;
            this.mode = mode;
        }

        public enum Mode {
            INSERT,
            SAVE
        }

        @Override
        public MethodSpec methodSpec() {
            String name;
            switch (mode){
                case INSERT:
                    name = "getInsertStatementQuery";
                    break;
                case SAVE:
                    name = "getSaveStatementQuery";
                    break;
                default:
                    name = "";
                    break;
            }

            CodeBlock.Builder codeBuilder = CodeBlock.builder();
            codeBuilder.add("INSERT ");
            if (mode != Mode.SAVE) {
                if (!tableDefinition.insertConflictActionName.isEmpty()) {
                    codeBuilder.add("OR "+tableDefinition.insertConflictActionName+" ");
                }
            } else {
                codeBuilder.add("OR "+ConflictAction.REPLACE+" ");
            }
            codeBuilder.add("INTO "+StringUtils.quote(tableDefinition.associationalBehavior().name)+"(");

            List<ColumnDefinition> columnDefinitions = tableDefinition.sqlColumnDefinitions();
            for(int index=0;index<columnDefinitions.size();index++) {
                ColumnDefinition columnDefinition = columnDefinitions.get(index);
                if (index > 0) codeBuilder.add(",");
                codeBuilder.add(columnDefinition.insertStatementColumnName());
            }

            codeBuilder.add(") VALUES (");

            for(int index=0;index<columnDefinitions.size();index++) {
                ColumnDefinition columnDefinition = columnDefinitions.get(index);
                if (index > 0) codeBuilder.add(",");
                codeBuilder.add(columnDefinition.insertStatementValuesString());
            }

            codeBuilder.add(")");

            return MethodSpec
                    .methodBuilder(name)
                    .returns(String.class)
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addStatement("return \"" + codeBuilder.build() + "\"")
                    .build();
        }
    }

    public static class UpdateStatementQueryMethod implements MethodDefinition {
        private final TableDefinition tableDefinition;

        public UpdateStatementQueryMethod(TableDefinition tableDefinition) {
            this.tableDefinition = tableDefinition;
        }

        @Override
        public MethodSpec methodSpec() {
            CodeBlock.Builder codeBuilder = CodeBlock.builder();

            codeBuilder.add("UPDATE");
            if (!tableDefinition.updateConflictActionName.isEmpty()) {
                codeBuilder.add(" OR " + tableDefinition.updateConflictActionName);
            }
            codeBuilder.add(" "+StringUtils.quote(tableDefinition.associationalBehavior().name)+" SET ");

            // can only change non primary key values.
            List<ColumnDefinition> columnDefinitions = tableDefinition.sqlColumnDefinitions();
            for(int index=0;index<columnDefinitions.size();index++) {
                ColumnDefinition columnDefinition = columnDefinitions.get(index);
                if (index > 0) codeBuilder.add(",");
                codeBuilder.add(columnDefinition.updateStatementBlock());
            }

            codeBuilder.add(" WHERE ");

            // primary key values used as WHERE
            columnDefinitions = tableDefinition.columnDefinitions.stream().filter(columnDefinition -> columnDefinition.type.isPrimaryField() || tableDefinition.type.isVirtual()).collect(Collectors.toList());
            for(int index=0;index<columnDefinitions.size();index++) {
                ColumnDefinition columnDefinition = columnDefinitions.get(index);
                if (index > 0) codeBuilder.add(" AND ");
                codeBuilder.add(columnDefinition.updateStatementBlock());
            }

            return MethodSpec
                    .methodBuilder("getUpdateStatementQuery")
                    .returns(String.class)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addAnnotation(Override.class)
                    .addStatement("return \"" + codeBuilder.build() + "\"")
                    .build();

        }
    }

    public static class DeleteStatementQueryMethod implements MethodDefinition {
        private final TableDefinition tableDefinition;

        public DeleteStatementQueryMethod(TableDefinition tableDefinition) {
            this.tableDefinition = tableDefinition;
        }

        @Override
        public MethodSpec methodSpec() {
            CodeBlock.Builder codeBuilder = CodeBlock.builder();
            codeBuilder.add("DELETE FROM "+StringUtils.quote(tableDefinition.associationalBehavior().name)+" WHERE ");

            // primary key values used as WHERE
            List<ColumnDefinition> columnDefinitions = tableDefinition.columnDefinitions.stream().filter(columnDefinition -> columnDefinition.type.isPrimaryField() || tableDefinition.type.isVirtual()).collect(Collectors.toList());

            for(int index=0;index<columnDefinitions.size();index++) {
                ColumnDefinition columnDefinition = columnDefinitions.get(index);
                if (index > 0) codeBuilder.add(" AND ");
                codeBuilder.add(columnDefinition.updateStatementBlock());
            }

            return MethodSpec
                    .methodBuilder("getDeleteStatementQuery")
                    .returns(String.class)
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addStatement("return \"" + codeBuilder + "\"")
                    .build();
        }
    }

    /**
     * Description:
     */
    public static class LoadFromCursorMethod implements MethodDefinition {
        public static final String PARAM_CURSOR = "cursor";
        private final EntityDefinition entityDefinition;

        public LoadFromCursorMethod(EntityDefinition entityDefinition) {
            this.entityDefinition = entityDefinition;
        }

        @Override
        public MethodSpec methodSpec() {
            MethodSpec.Builder methodBuilder = MethodSpec
                    .methodBuilder("loadFromCursor")
                    .returns(entityDefinition.parameterClassName())
                    .addParameter(ParameterSpec.builder(ClassNames.FLOW_CURSOR, PARAM_CURSOR).build())
                    .addParameter(ParameterSpec.builder(ClassNames.DATABASE_WRAPPER, ModelUtils.wrapper).build())
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addStatement("$1T "+ModelUtils.variable+" = new $1T()", entityDefinition.parameterClassName());

            AtomicInteger index = new AtomicInteger(0);
            NameAllocator nameAllocator = new NameAllocator(); // unique names
            entityDefinition.columnDefinitions.forEach(columnDefinition -> {
                methodBuilder.addCode(columnDefinition.getLoadFromCursorMethod(true, index, nameAllocator));
                index.incrementAndGet();
            });

            CodeBlock.Builder codeBuilder = CodeBlock.builder();
            if (entityDefinition instanceof TableDefinition) {
                ((TableDefinition) entityDefinition).oneToManyDefinitions.stream()
                        .filter(OneToManyDefinition::isLoad)
                        .forEach(oneToManyDefinition -> oneToManyDefinition.writeLoad(codeBuilder));
            }
            methodBuilder.addCode(codeBuilder.build());

            if (entityDefinition.implementsLoadFromCursorListener()) {
                methodBuilder.addStatement(ModelUtils.variable + ".onLoadFromCursor("+PARAM_CURSOR+")");
            }

            methodBuilder.addStatement("return "+ModelUtils.variable);

            return methodBuilder.build();
        }
    }

    /**
     * Description:
     */
    public static class OneToManyDeleteMethod implements MethodDefinition {
        private final TableDefinition tableDefinition;
        private final boolean isPlural;

        private final String variableName;
        private final TypeName typeName ;
        private final String methodName;

        public OneToManyDeleteMethod(TableDefinition tableDefinition, boolean isPlural) {
            this.tableDefinition = tableDefinition;
            this.isPlural = isPlural;

            variableName = isPlural? "models" : ModelUtils.variable;
            typeName = !isPlural? tableDefinition.elementClassName :
                    ParameterizedTypeName.get(ClassName.get(Collection.class), WildcardTypeName.subtypeOf(tableDefinition.elementClassName));

            methodName = "delete" + (isPlural? "All" : "");
        }

        @Override
        public MethodSpec methodSpec() {
            boolean shouldWrite = tableDefinition.oneToManyDefinitions.stream().anyMatch(OneToManyDefinition::isDelete);
            if (shouldWrite || tableDefinition.cachingBehavior.cachingEnabled) {
                TypeName returnTypeName = isPlural? TypeName.LONG : TypeName.BOOLEAN;

                MethodSpec.Builder methodBuilder = MethodSpec
                        .methodBuilder(methodName)
                        .returns(returnTypeName)
                        .addParameter(typeName, variableName)
                        .addAnnotation(Override.class)
                        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                        .addParameter(ClassNames.DATABASE_WRAPPER, ModelUtils.wrapper);

                if (tableDefinition.cachingBehavior.cachingEnabled) {
                    methodBuilder.addStatement("cacheAdapter.removeModel"+(isPlural? "s" : "")+"FromCache("+variableName+")");
                }

                methodBuilder.addStatement("$T successful = super."+methodName+"("+variableName+""+ ColumnAccessCombiner.wrapperCommaIfBaseModel(true)+")", returnTypeName);

                if (isPlural && !tableDefinition.oneToManyDefinitions.isEmpty()) {
                    methodBuilder.beginControlFlow("for " + " ("+"$T model: models"+")", tableDefinition.elementClassName);
                    tableDefinition.oneToManyDefinitions.forEach(oneToManyDefinition -> oneToManyDefinition.writeDelete(methodBuilder));
                    methodBuilder.endControlFlow();
                } else {
                    tableDefinition.oneToManyDefinitions.forEach(oneToManyDefinition -> oneToManyDefinition.writeDelete(methodBuilder));
                }

                methodBuilder.addStatement("return successful");

                return methodBuilder.build();
            }
            return null;
        }
    }

    /**
     * Description: Overrides the save, update, and insert methods if the [com.dbflow5.annotation.OneToMany.Method.SAVE] is used.
     */
    public static class OneToManySaveMethod implements MethodDefinition {
        public static final String METHOD_SAVE = "save";
        public static final String METHOD_UPDATE = "update";
        public static final String METHOD_INSERT = "insert";

        private final TableDefinition tableDefinition;
        private final String methodName;
        private final boolean isPlural;

        private final String variableName;
        private final TypeName typeName;

        private final String fullMethodName;

        public OneToManySaveMethod(TableDefinition tableDefinition, String methodName, boolean isPlural) {
            this.tableDefinition = tableDefinition;
            this.methodName = methodName;
            this.isPlural = isPlural;

            variableName = isPlural? "models" : ModelUtils.variable;
            typeName = !isPlural? tableDefinition.elementClassName :
                    ParameterizedTypeName.get(ClassName.get(Collection.class), WildcardTypeName.subtypeOf(tableDefinition.elementClassName));

            fullMethodName = methodName + (isPlural? "All" : "");
        }

        @Override
        public MethodSpec methodSpec() {
            if (!tableDefinition.oneToManyDefinitions.isEmpty() || tableDefinition.cachingBehavior.cachingEnabled) {
                TypeName retType = TypeName.BOOLEAN;
                String retStatement = "successful";
                if (isPlural) {
                    retType = ClassName.LONG;
                    retStatement = "count";
                } else if (fullMethodName.equals(METHOD_INSERT)) {
                    retType = ClassName.LONG;
                    retStatement = "rowId";
                }

                MethodSpec.Builder methodBuilder = MethodSpec
                        .methodBuilder(fullMethodName)
                        .returns(retType)
                        .addParameter(ParameterSpec.builder(typeName, variableName).build())
                        .addParameter(ClassNames.DATABASE_WRAPPER, ModelUtils.wrapper)
                        .addAnnotation(Override.class)
                        .addModifiers(Modifier.PUBLIC, Modifier.FINAL);

                CodeBlock.Builder codeBuilder = CodeBlock.builder();

                if (isPlural) {
                    codeBuilder.add("long count = ");
                } else if (fullMethodName.equals(METHOD_INSERT)) {
                    codeBuilder.add("long rowId = ");
                } else if (fullMethodName.equals(METHOD_UPDATE) || fullMethodName == METHOD_SAVE) {
                    codeBuilder.add("boolean successful = ");
                }
                codeBuilder.addStatement("super."+fullMethodName+"("+variableName+""+ColumnAccessCombiner.wrapperCommaIfBaseModel(true)+")");

                if (tableDefinition.cachingBehavior.cachingEnabled) {
                    codeBuilder.addStatement("cacheAdapter.storeModel"+(isPlural? "s" : "")+"InCache("+variableName+")");
                }

                methodBuilder.addCode(codeBuilder.build());

                List<OneToManyDefinition> filteredDefinitions = new ArrayList<>();

                tableDefinition.oneToManyDefinitions.forEach(oneToManyDefinition -> {
                    if(oneToManyDefinition.isSave()) {
                        filteredDefinitions.add(oneToManyDefinition);
                    }
                });

                if (isPlural && !filteredDefinitions.isEmpty()) {
                    methodBuilder.beginControlFlow("for ($T model: models)", tableDefinition.elementClassName);
                    filteredDefinitions.forEach(oneToManyDefinition -> {
                        switch (methodName){
                            case METHOD_SAVE:
                                oneToManyDefinition.writeSave(methodBuilder);
                                break;
                            case METHOD_UPDATE:
                                oneToManyDefinition.writeUpdate(methodBuilder);
                                break;
                            case METHOD_INSERT:
                                oneToManyDefinition.writeInsert(methodBuilder);
                                break;
                            default:
                                break;
                        }
                    });
                    methodBuilder.endControlFlow();
                } else {
                    filteredDefinitions.forEach(oneToManyDefinition -> {
                        switch (methodName){
                            case METHOD_SAVE:
                                oneToManyDefinition.writeSave(methodBuilder);
                                break;
                            case METHOD_UPDATE:
                                oneToManyDefinition.writeUpdate(methodBuilder);
                                break;
                            case METHOD_INSERT:
                                oneToManyDefinition.writeInsert(methodBuilder);
                                break;
                            default:
                                break;
                        }
                    });
                }

                methodBuilder.addStatement("return " + retStatement);

                return methodBuilder.build();
            } else {
                return null;
            }
        }
    }

    /**
     * Description: Creates a method that builds a clause of ConditionGroup that represents its primary keys. Useful
     * for updates or deletes.
     */
    public static class PrimaryConditionMethod implements MethodDefinition {
        private EntityDefinition tableDefinition;

        public PrimaryConditionMethod(EntityDefinition tableDefinition) {
            this.tableDefinition = tableDefinition;
        }

        @Override
        public MethodSpec methodSpec() {
            CodeBlock.Builder codeBuilder = CodeBlock.builder();

            codeBuilder.addStatement("$T clause = $T.clause()", ClassNames.OPERATOR_GROUP, ClassNames.OPERATOR_GROUP);
            tableDefinition.primaryColumnDefinitions().forEach(columnDefinition -> {
                CodeBlock.Builder code = CodeBlock.builder();
                columnDefinition.appendPropertyComparisonAccessStatement(code);
                codeBuilder.add(code.build());
            });

            return MethodSpec
                    .methodBuilder("getPrimaryConditionClause")
                    .returns(ClassNames.OPERATOR_GROUP)
                    .addParameter(ParameterSpec.builder(tableDefinition.parameterClassName(), ModelUtils.variable).build())
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addCode(codeBuilder.build())
                    .addStatement("return clause")
                    .build();
        }
    }
}